﻿using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.WowAddonStudio.Lua.Parser;

using NUnit.Framework;

namespace Microsoft.WowAddonStudio.Lua.Tests.Parser
{
    /// <summary>
    /// Contains tests that test the scanner that contains the lexer functionality and tokenizes
    /// the source.
    /// </summary>
    [TestFixture]
    public class ScannerTest
    {
        private Scanner scanner;

        private string source;
        private ParsedToken[] expected, actual;

        [SetUp]
        public void SetUp()
        {
            scanner = new Scanner
            {
                ErrorHandler = new ConsoleErrorHandler()
            };
        }

        [Test(Description = "Tests that valid Lua identifiers are correctly tokenized.")]
        public void IdentifiersTest()
        {
            source = @"foo
_bar
FUNCTION6
foo69bar
";

            expected = new[]
                           {
                               new ParsedToken(Tokens.IDENTIFIER, new LexLocation(0, 0, 0, 3)),
                               new ParsedToken(Tokens.IDENTIFIER, new LexLocation(1, 0, 1, 4)),
                               new ParsedToken(Tokens.IDENTIFIER, new LexLocation(2, 0, 2, 9)),
                               new ParsedToken(Tokens.IDENTIFIER, new LexLocation(3, 0, 3, 8))
                           };

            this.AssertSource();
        }

        [Test(Description = "Tests that all valid Lua keywords are tokenized as keywords.")]
        public void KeywordsTest()
        {
            source = @"and
break
do        
else      
elseif
end       
false     
for       
function  
if
in       
local     
nil       
not       
or
repeat    
return    
then      
true      
until     
while";

            expected = new[]
                           {
                               new ParsedToken(Tokens.KWAND, new LexLocation(0, 0, 0, 3)),
                               new ParsedToken(Tokens.KWBREAK, new LexLocation(1, 0, 1, 5)),
                               new ParsedToken(Tokens.KWDO, new LexLocation(2, 0, 2, 2)),
                               new ParsedToken(Tokens.KWELSE, new LexLocation(3, 0, 3, 4)),
                               new ParsedToken(Tokens.KWELSEIF, new LexLocation(4, 0, 4, 6)),
                               new ParsedToken(Tokens.KWEND, new LexLocation(5, 0, 5, 3)),
                               new ParsedToken(Tokens.KWFALSE, new LexLocation(6, 0, 6, 5)),
                               new ParsedToken(Tokens.KWFOR, new LexLocation(7, 0, 7, 3)),
                               new ParsedToken(Tokens.KWFUNCTION, new LexLocation(8, 0, 8, 8)),
                               new ParsedToken(Tokens.KWIF, new LexLocation(9, 0, 9, 2)),
                               new ParsedToken(Tokens.KWIN, new LexLocation(10, 0, 10, 2)),
                               new ParsedToken(Tokens.KWLOCAL, new LexLocation(11, 0, 11, 5)),
                               new ParsedToken(Tokens.KWNIL, new LexLocation(12, 0, 12, 3)),
                               new ParsedToken(Tokens.KWNOT, new LexLocation(13, 0, 13, 3)),
                               new ParsedToken(Tokens.KWOR, new LexLocation(14, 0, 14, 2)),
                               new ParsedToken(Tokens.KWREPEAT, new LexLocation(15, 0, 15, 6)),
                               new ParsedToken(Tokens.KWRETURN, new LexLocation(16, 0, 16, 6)),
                               new ParsedToken(Tokens.KWTHEN, new LexLocation(17, 0, 17, 4)),
                               new ParsedToken(Tokens.KWTRUE, new LexLocation(18, 0, 18, 4)),
                               new ParsedToken(Tokens.KWUNTIL, new LexLocation(19, 0, 19, 5)),
                               new ParsedToken(Tokens.KWWHILE, new LexLocation(20, 0, 20, 5)),
                           };

            this.AssertSource();
        }
        
        [Test(Description = "Tests operators and other tokens valid in the Lua language.")]
        public void OperatorsAndOtherTokensTest()
        {
            source = @"+
-
*
/
%
^
#
=
==
~=
<=
>=
<>
()
{}
[]
;
:
,
.
..
...";
            expected = new[]
                           {
                               new ParsedToken(Tokens.PLUS, new LexLocation(0, 0, 0, 1)),
                               new ParsedToken(Tokens.MINUS, new LexLocation(1, 0, 1, 1)),
                               new ParsedToken(Tokens.ASTERISK, new LexLocation(2, 0, 2, 1)),
                               new ParsedToken(Tokens.SLASH, new LexLocation(3, 0, 3, 1)),
                               new ParsedToken(Tokens.PERCENT, new LexLocation(4, 0, 4, 1)),
                               new ParsedToken(Tokens.CARET, new LexLocation(5, 0, 5, 1)),
                               new ParsedToken(Tokens.POUND, new LexLocation(6, 0, 6, 1)),
                               new ParsedToken(Tokens.EQUAL, new LexLocation(7, 0, 7, 1)),
                               new ParsedToken(Tokens.EQ, new LexLocation(8, 0, 8, 2)),
                               new ParsedToken(Tokens.NEQ, new LexLocation(9, 0, 9, 2)),
                               new ParsedToken(Tokens.LTE, new LexLocation(10, 0, 10, 2)),
                               new ParsedToken(Tokens.GTE, new LexLocation(11, 0, 11, 2)),

                               new ParsedToken(Tokens.LT, new LexLocation(12, 0, 12, 1)),
                               new ParsedToken(Tokens.GT, new LexLocation(12, 1, 12, 2)),

                               new ParsedToken(Tokens.LPAREN, new LexLocation(13, 0, 13, 1)),
                               new ParsedToken(Tokens.RPAREN, new LexLocation(13, 1, 13, 2)),

                               new ParsedToken(Tokens.LBRACE, new LexLocation(14, 0, 14, 1)),
                               new ParsedToken(Tokens.RBRACE, new LexLocation(14, 1, 14, 2)),

                               new ParsedToken(Tokens.LBRACKET, new LexLocation(15, 0, 15, 1)),
                               new ParsedToken(Tokens.RBRACKET, new LexLocation(15, 1, 15, 2)),

                               new ParsedToken(Tokens.SEMICOLON, new LexLocation(16, 0, 16, 1)),
                               new ParsedToken(Tokens.COLON, new LexLocation(17, 0, 17, 1)),
                               new ParsedToken(Tokens.COMMA, new LexLocation(18, 0, 18, 1)),
                               new ParsedToken(Tokens.DOT, new LexLocation(19, 0, 19, 1)),
                               new ParsedToken(Tokens.DOTDOT, new LexLocation(20, 0, 20, 2)),
                               new ParsedToken(Tokens.ELLIPSIS, new LexLocation(21, 0, 21, 3)),
                           };

            this.AssertSource();
        }

        [Test(Description = "Tests that all escape sequences in a single quoted string get tokenized as strings.")]
        public void SingleQuotedStringEscapeSequencesTest()
        {
            source = @"' \a \b \f \n \r \t \v \\ \' \n '";

            this.AssertString(source);
        }

        [Test(Description = "Tests that all escape sequences in a single quoted string get tokenized as strings.")]
        public void DoubleQuotedStringEscapeSequencesTest()
        {
            source = @""" \a \b \f \n \r \t \v \\ \"" \n """;

            this.AssertString(source);
        }

        [Test(Description = "Tests that invalid escape sequences still get tokenized as strings.")]
        public void InvalidEscapeSequenceTest()
        {
            source = @"' \c\d\e '";

            this.AssertString(source);
            
            Assert.AreEqual(3, scanner.ErrorHandler.WarningCount);
        }

        [Test(Description = "Tests numerical escape sequences.")]
        public void StringNumericalEscapeSequencesTest()
        {
            this.AssertString(@"'alo\n123""'");       // alo\n123"
            this.AssertString(@"""alo\n123\""""");       // alo\n123\"
            this.AssertString(@"'\97lo\10\04923""'"); // \97lo\10\04923"
            this.AssertString(@"'\049\050\051\127'"); // \049\050\051\127
        }

        [Test(Description = "Tests long bracket strings.")]
        public void LongBracketStringsTest()
        {
            this.AssertString(@"[[alo
123""]]"); // alo\n123"

            this.AssertString(@"[==[alo
123""]==]"); // alo\n123"

            this.AssertString(@"[===[alo]=]helo]==]
jujj]===]"); // [alo]=]helo]==]\njujj

            this.AssertString(@"[==[function foo() print([===[ long bracket string ]===]) end]==]");
        }

        [Test(Description = "Tests the numerical constants.")]
        public void NumericalConstantsTest()
        {
            // 3   3.0   3.1416   314.16e-2   0.31416E1   0xff   0x56
            AssertNumber("3");
            AssertNumber("3.0");
            AssertNumber("3.1416");
            AssertNumber(".1416");
            AssertNumber("3.");
            AssertNumber("314.16e-2");
            AssertNumber("0.31416E1");
            AssertNumber("0xff");
            AssertNumber("0x56");
            AssertNumber("0xc");
            AssertNumber("0xaabbcc12345");
            AssertNumber("0x00a6");

            // Invalid numbers
            AssertNumber("314.16-2", false);
            AssertNumber("0x", false);
            AssertNumber("3,1415", false);
            AssertNumber("314.15e", false);
        }

        [Test(Description = "Tests the line and block comments.")]
        public void CommentsTest()
        {
            AssertComment("-- This is a comment");
            AssertComment(@"-- First row of a comment
-- and another one");
            AssertComment(@"--[=[
This is a long comment

That runs...[[Other long brackets are ignored]]

and runs...]=]");
        }

        [Test(Description = "Tests how the scanner handles invalid characters.")]
        public void InvalidCharacterTest()
        {
            source = "fú = 5";

            ParseSource();
            Assert.AreEqual(1, scanner.ErrorHandler.ErrorCount);
        }

        private void AssertComment(string value)
        {
            Console.WriteLine("AssertComment: " + value);

            actual = ParseTokens(value).ToArray();
            Assert.AreEqual(0, actual.Length, "Token returned, string is not just a comment.");
        }

        private void AssertNumber(string value)
        {
            this.AssertNumber(value, true);
        }

        private void AssertNumber(string value, bool isNumber)
        {
            Console.WriteLine("AssertNumber: {0} (IsNumber: {1})", value, isNumber);

            actual = ParseTokens(value).ToArray();

            if (isNumber)
            {
                Assert.AreEqual(1, actual.Length, "Parse returned more than one token.");
                Assert.AreEqual(Tokens.NUMBER, actual[0].Token, "Source is not a number.");
            }
            else
            {
                if (actual.Length == 1 && actual[0].Token == Tokens.NUMBER)
                    Assert.Fail("Value incorrectly parsed as number.");
            }
        }

        private void AssertString(string value)
        {
            Console.WriteLine("AssertString: " + value);

            actual = ParseTokens(value).ToArray();
            foreach (var parsedToken in actual)
                Assert.AreEqual(Tokens.STRING, parsedToken.Token);

        }

        private void AssertSource()
        {
            Console.WriteLine("AssertSource: " + source);
            ParseSource();
            Assert.AreEqual(expected, actual);
        }

        private void ParseSource()
        {
            actual = ParseTokens(source).ToArray();
        }

        private IEnumerable<ParsedToken> ParseTokens(string value)
        {
            return scanner.SetSource(value).GetTokens();
        }
    }
}